#!/usr/bin/python3

import json, logging, re, argparse, sqlite3, json, xml.etree.ElementTree as ET, sys, textwrap
from pathlib import Path
from typing import Any
from lib import XPath

from argparse import RawDescriptionHelpFormatter

# call with: python test.py -h

class SmartDescriptionFormatter(argparse.RawDescriptionHelpFormatter):
  #def _split_lines(self, text, width): # RawTextHelpFormatter, although function name might change depending on Python
  def _fill_text(self, text, width, indent): # RawDescriptionHelpFormatter, although function name might change depending on Python
    #print("splot",text)
    if text.startswith('R|'):
      paragraphs = text[2:].splitlines()
      rebroken = [argparse._textwrap.wrap(tpar, width) for tpar in paragraphs]
      #print(rebroken)
      rebrokenstr = []
      for tlinearr in rebroken:
        if (len(tlinearr) == 0):
          rebrokenstr.append("")
        else:
          for tlinepiece in tlinearr:
            rebrokenstr.append(tlinepiece)
      #print(rebrokenstr)
      return ''.join(rebrokenstr) #(argparse._textwrap.wrap(text[2:], width))
    # this is the RawTextHelpFormatter._split_lines
    #return argparse.HelpFormatter._split_lines(self, text, width)
    return argparse.RawDescriptionHelpFormatter._fill_text(self, text, width, indent)



"""
Try
"""

#### =======================================================================================================
####     Open function
#### =======================================================================================================
def xread( path:str, log:logging.Logger = None ) -> str :

    #### 匹配路径
    patt = r'(?:(?P<protocol>[\w]+):)?(?P<path>[\w./]+)(?P<field>[#][^?]+)?(?P<parameter>[?](.*))?'
    m = re.match(patt, path)
    assert m, f'Cannot match Path: {str}'

    #### 处理匹配结果
    path = m.group('path')
    assert Path(path).exists(), f'Cannot open file: {path}'

    protocol = m.group('protocol')
    if not protocol: protocol = 'file'

    field = m.group('field')
    if field: field = field[1:].split('/')      # 去除'#'后按'/'拆分

    parameter = m.group('parameter')
    if parameter: parameter = [cond.split('=') for cond in parameter[1:].split('&')]     # 去除'?'后按'&'拆分

    if log: log.info(f'xread: protocol={protocol}; path={path}; field={field}; parameter={parameter}')

    rst = ""
    #### 分条件处理
    if protocol == "sqlite":
        assert field and parameter and len(field)==2 and len(parameter)==1, f'Cannot support <NULL> field and parameter for sqlite read'
        try:
            conn = sqlite3.connect(path)
            cur = conn.cursor()
            sql = f"""SELECT "{field[1]}" FROM "{field[0]}" WHERE "{parameter[0][0]}"='{parameter[0][1]}' """
            cur.execute(sql)
            r = cur.fetchone()
            assert r, f'Cannot find field for SQL: {sql}'
            rst = r[0]
            conn.close()
        except Exception:
            if log: log.fatal(f'Sqlite error for: {sql}')

    elif protocol == "file":
        with open(path, "r", encoding="utf-8") as f:
            rst = f.read()

    else:
        if log: log.fatal(f"Cannot support this protocol: {protocol}")

    return rst

def parse_src_dst(args):
    src = args.input
    assert src!= "", f"Output file name cannot be empty"
    assert Path(src).exists(), f"Cannot find valid file: {src}"
    dst = args.output if args.output else src
    return (src,dst)


def parse_vars(varss:list[str]) -> dict[str, Any]:
    """从vars参数中解析为dict"""

    def parse_key_value(param:str) -> dict:
        """解析key=json格式"""
        k, s, v = param.partition("=")
        if v[0]=="[":
            v = v[1:-1].split(",")
        else:
            try: v = json.loads(v)
            except ValueError: pass
        return {k:v}

    rst = {}
    for d in varss:  # 更新数据
        rst.update(parse_key_value(d))

    return rst



#### =======================================================================================================
#### Global Variable and set
#### =======================================================================================================
ET.register_namespace('', "http://www.w3.org/2000/svg")
ET.register_namespace('xlink', "http://www.w3.org/1999/xlink")

logger = logging.getLogger(__file__)
logger.setLevel(logging.DEBUG)  # 设置全局显示级别

content = """{
    "src" : "example.svg",                          // MUST: 输入SVG文件名(相对当前脚本路径)
    "out" : "example.svg",                          // Option: 输出SVG文件名(相对当前脚本路径)。缺省不输出
    "inject" : {                                    // Option:  缺省不注入
        "src" : "example.html",                     //  - MUST: 注入文件名(相对当前脚本路径)
        "out" : "example.html",
        "label" : "{{SVG_INJECT}}"                  // 忽略为使用src名替代
    },

    "links" : {                                     // Optional: 添加的超链接信息块
        "linkPatt" : "queryContent('{1}', '{2}')",  // MUST: 链接替换Pattern格式。其中{0}指sourceMap的key, {1},{2},...指sourceMap的value
        "sourceMap" : {                             // Optional: 映射表 
            "Text0" : "Title Content",              // Value等效于: {"title":"Title Content"}
            "Text1" :["Param1", "Param2"],          // Key: SVG中的text文字; Value: 函数参数。默认左键
            "Text2" : {
                // 4种添加项处理(Insert/Update/Delete)操作信息
                // * 忽略的项表示不对SVG文件中该选处理
                // * 每一项的'value'支持两种格式：1) dict格式：见下描述；2) str格式：直接替换
                // * 每一项中若忽略"linkPatt"，则使用顶层linkPatt
                // * 每一项的'param'支持2种格式。1) list: 参数列表；2) str: 相当于[str]
                // * linkPatt的替换结果为"", 表示删除该属性
                // * 每一项的value为""时，表示删除该属性
                "id" : "identify",                  // 标识名
                "title" : "Title Context",          // 可以使用unicode特殊字符。例如: \u25CF标识小圆点；\u000A表示换行
                "href" : {                          // 超链接
                    "linkPatt" : "try.html",
                    "params" : ["parameters"]       // 超链接内容。默认: javascript:void
                }, 
                "left" : {                          // 对应onclick。onclick早于href相应
                    "linkPatt" : "alert('{0}')",
                    "params" : ["parameters"]       // 超链接内容。默认去除
                },
                "right" : {                         // 对应onContextMenu
                    "linkPatt" : "console.log('{0}', '{1}')",
                    "params" : ["parameters"]       // 超链接内容。默认去除
                    // 右键函数参考
                    // const proc = ev =>{
                    //     ev = window.event
                    //     ev.preventDefault()      // 阻止浏览器默认右键
                    //     ......
                    // }
                }
            }
        }
    },

    "layer" : [                                     // 合并入的SVG文件
        {
            "src" : "example.svg",                  // MUST: 输入SVG文件名(相对当前脚本路径)。""代表去除该图层（若有的话)
            "name" : "Layer",                       // Optional: 输出SVG.g.id。不设置则使用src名。这时,src不能为空
            "x" : 0,                                // <g x="x">
	        "y" : 0,                                // <g y="y">
	        "display" : "none",                     // 默认显示属性。忽略为"none"
	        "delBkRect" : true,                     // 是否删除第一个rect元素（eDraw用来作为背景)。忽略为true

            "links" : {                             // Optional: 添加的超链接信息块
            }
        }
    ]
}
"""


#### =======================================================================================================
#### SVG文件处理操作定义
#### =======================================================================================================
class SvgFile:
    """读取、修改、保存SVG的操作类"""

    @staticmethod
    def read(self, xmlFileName:str): return SvgFile().read(xmlFileName)

    def __init__(self, name:str=""):
        """
        * name: SVG标识名
        """
        self.root : ET.Element = ET.Element('None')  # Element
        self.name = name

    
    def read(self, xmlFileName:str) -> None:
        """读取XML文件内容到SvgFile实例中
        * xmlFileName: 需要操作的XML文件名(相对于当前shell路径，允许绝对路径)
        """

        assert Path(xmlFileName).exists(), f"Cannot find file: {xmlFileName}!"

        def namespace(ele):
            """获取ElementTree元素的命名空间"""
            m = re.match(r'\{.*\}', ele.tag)
            return m.group(0) if m else ""

        self.root = ET.parse(xmlFileName).getroot()
        self.name = XPath(xmlFileName).name
        self.ns = namespace(self.root)

        if self.ns:     # 存在namespace
            svgText = ET.tostring( self.root, encoding='unicode' )      # 无法直接去除，使用文本处理
            svgText = re.sub( r'xmlns="[^"]+"', '', svgText, 1 )        # 去除xmlns属性
            self.root = ET.fromstring(svgText)                          # 文本转回到elementtree

        return self
    
    def removeEdrawBackground(self):
        """去除edraw输出SVG的背景rect"""
        width = float(self.root.get('width', 0))        # 页面宽度
        height = float(self.root.get('height', 0))      # 页面高度

        page_g = self.root.find("g")           # edraw输出的page页面为第一个g
        if not page_g: return self             # 无page页
        if not page_g.get('id', "Unknow")[:4]=="page": return self    # 是page页才需要删除，否则没必要
        
        rect = page_g.find('rect')      # 第一个rect
        if not width-float(rect.get('width', 1000)) <= 50: return self        # 宽度不匹配，不是背景
        if not height-float(rect.get('height', 1000)) <= 50: return self      # 高度不匹配，不是背景

        page_g.remove(rect)     # 去除背景
        return self
    
    def edrawIdentifyProc(self):
        """标识eDraw输出SVG的各层次ID"""

        self.root.set('id', self.name)          # 标识SVG

        page_g = self.root.find("g")            # edraw输出的page页面为第一个g
        if not page_g: return self              # 无page页
        page_g.set('id', f'page_{self.name}')             # 标识顶层g

        for g in page_g.findall('g'):
            if g.get('id', 'Unknow')[:5]!="group": continue     # 循环条件
            for gs in g.findall('g'):
                if gs.get('id', 'Unknow')[:5]!="shape": continue # 默认非group
                text = gs.find('text[@style]')
                if not text: continue 
                name = text.findtext('tspan')
                if name==None or name.find('Layer:')!=0: continue # 约定字符串格式: Layer:xxx

                style = text.get('style')

                # 隐藏图层
                if style.find('fill:#ffffff;font-family:华文彩云;font-size:12')!=-1 or style.find('fill:#ffffff;font-family:STCaiyun;font-size:12')!=-1: 
                    g.set('id', name[6:].strip())
                    sty = re.sub(r'display:[^;]+;', "display:none;", g.get("style", "display:none"))
                    g.set('style', sty)


        return self

    
    
    def toLayer(self, layerName:str, x:int=0, y:int=0, display:str="none", delBkRect:bool=True) -> None:
        """SVG文件内容转化为<g>元素
        * layerName: g元素的id属性
        * x: X坐标位置
        * y: Y坐标位置
        """

        # 当前SVG由eDraw导出
        headPatt = re.compile(r'^<svg [^>]+><defs />')
        tailPatt = re.compile(r'</svg>$')

        headRepl = f'<g id="{layerName}" transform="matrix(1,0,0,1,{x},{y})" style="display:{display}">'
        svgText = ET.tostring( self.root, encoding='unicode' )
        svgText = re.sub( headPatt, headRepl, svgText, 1 )
        svgText = re.sub( tailPatt, '</g>', svgText, 1 )
        self.root = ET.fromstring(svgText)
        self.ns = ""

        #### 去掉第一个Rect背景框
        if delBkRect:
            page_g = self.root.find("g")
            if page_g and page_g.get("id")[:4]=="page":
                rect = page_g.find("rect")
                page_g.remove(rect)
                logger.info(f"Remove a backgroud rect")

        self.name = layerName
        logger.info(f"Translate SVG file into SVG symbol: {layerName}")

        return self
    
    def addSvgComment(self, comments:list[dict[str,Any]], layerPath:str="."):
        """为Svg.Text标签添加Comment信息
        * comments: Comment信息
        * layerPath: 插入的XML层次路径(相对根节点)
        """

        def getText(dom): return dom.find(".//*").strip()

        def firstMatch(name:str, comments:list[dict[str,Any]]) -> tuple[dict, list]:
            """在comments中查找第一个'name'的正则匹配name的comment."""
            if name == '': return (None, None)  # 无效标签
            for cmt in comments:        # 一个个和cmt匹配
                mobj = re.fullmatch(cmt.get('patt', ''), name)     # 匹配
                if not mobj: continue   # 不匹配
                return (cmt, list(mobj.groups()))  # 获取匹配参数列表
            return (None, None)

        def aDom(textDom, name:str):
            """找到<a>元素DOM"""
            opt_dom = textDom

            # tspan
            tspan_dom = opt_dom.find('tspan')
            if tspan_dom!=None: opt_dom = tspan_dom

            # a
            a_dom = opt_dom.find('a')
            if a_dom==None: 
                opt_dom.text = ''
                a_dom = ET.Element('a')
                a_dom.text = name
                opt_dom.append(a_dom)
            
            return opt_dom
            


        for textDom in self.root.find(layerPath).iter('text'):      # 遍历所有的text标签(包括子标签)
            name = getText(textDom)            # 获取text标签下的text内容
            cmt, params = firstMatch( name, comments )   # 获取参数列表
            if params==None: continue               # 匹配失败
            params.extend(cmt.get('param', []))     # 用户参数合并(多个选项共用)

            if 'id' in cmt:     # id标签处理
                textDom.attrib['id'] = cmt['id'].format(name, *params)

            a_dom = aDom(textDom, name)

            if 'title' in cmt:
                title = ET.Element("title")
                title.text = cmt['title'].format(name, *params)
                a_dom.append(title)

            if 'href' in cmt:
                a_dom.attrib['href'] = cmt['href'].format(name, *params)

            if 'left' in cmt:
                script = cmt['left'].format(name, *params)
                a_dom.attrib['onclick'] = f"javascript:{script}"

            if 'right' in cmt:
                script = cmt['right'].format(name, *params)
                a_dom.attrib['onContextMenu'] = f"javascript:{script}"

        return self


    def addLinks2(self, config:dict) -> 'SvgFile':
        return self.addLinks(config.get('sourceMap', {}), config.get('linkPatt', ""))

    def addLinks(self, sourceMap:dict[str, list[Any]], linkPatt:str) -> None:
        """添加超链接
        * sourceMap: 链接信息Dict
          - key: SVG中的原始字符串
          - value: 映射后的list表
        * linkPatt: 映射Pattern字符串(Python Format格式)
        """
        if sourceMap=={}: return self   # 空列表

        logger.debug(f"Start link process: pattern={linkPatt}")

        prefix = "{http://www.w3.org/2000/svg}" if self.root.tag=="{http://www.w3.org/2000/svg}svg" else ""
        stat, total = 0, 0
        for text in self.root.iter(f'{prefix}text'):
            
            #### 初始化内部变量
            tspan, a, title = None, None, None
            a_attrib = {}
            cnxt = text.text.strip()    # 获取text中的文字
            
            # case1: 文字直接在text下：清零该文字，然后append tspan元素
            if not cnxt:  # case2: 藏在tspan中，清零该文字，然后append a元素
                tspan = text.find(f'{prefix}tspan')
                cnxt = tspan.text.strip()
                if not cnxt: # case3: 藏在a中，清空tspan，然后追加a元素
                    a = tspan.find(f'{prefix}a')
                    a_attrib = a.attrib
                    cnxt = a.text.strip()
                    title = a.find(f'{prefix}title')

            total = total + 1
            if not cnxt in sourceMap: continue  # 跳过非需要添加link的文字

            logger.debug(f'Find a operator key: {cnxt}')

            #### 参数获取
            linkDict = sourceMap[cnxt]  # 读取链接信息

            if isinstance(linkDict, str): 
                linkDict = {"title":{"param":[], "linkPatt":linkDict}}
            if isinstance(linkDict, list): 
                linkDict = {"left": {"param":linkDict, "linkPatt":linkPatt}}
            assert isinstance(linkDict, dict), f"Link config only support Dict format. But current is: {linkDict}"
            

            #### 添加4种事件
            for (name, event, isScript) in [
                    ("left", "onclick", True),    
                    ("right", "onContextMenu", True),    
                    ("href", "href", False),    
                    ("title", "title", False),    
                ]:
                if name not in linkDict: continue  # 只处理JSON中标识出来的内容
                
                info = linkDict[name] # 获取链接信息
                if isinstance(info, str): info={"linkPatt":info, "param":[]}
                elif isinstance(info, list): info={"linkPatt":linkPatt, "param":info}
                assert isinstance(info, dict), f"Info only support dict format. Current info is: {info}"
                    
                lnkPatt = info.get("linkPatt", linkPatt)
                param = info.get("params", [])
                if isinstance(param, str): param = [param]
                assert isinstance(lnkPatt, str), f"linkPatt must be str type"
                assert isinstance(param, list), f"param must be list type"
                ## 替换特殊字符
                if name=="title":
                    script = lnkPatt
                else:
                    script = lnkPatt.format( cnxt, *param ) # 正则模板替换

                if name=="title": 
                    if script: 
                        if not title: title = ET.Element("title")
                        title.text = script
                    else:
                        title = None
                elif script: a_attrib[event] = f"javascript:{script}" if isScript else script
                else: a_attrib.pop(event)

            #### 添加ID属性
            a_attrib['id'] = cnxt

            # 构建a元素
            ele_a = ET.Element( 'a', a_attrib )
            ele_a.text = cnxt
           
            #### 构建title元素
            if title!=None: ele_a.append(title)

            # 构建tspan元素
            if tspan==None:  #之前无tspan
                tspan_new = ET.Element( 'tspan', {})
                text.tail = None  # 无tspan，文字一定属于text
            else:  #之前有tspan
                tspan_new = ET.Element( 'tspan', tspan.attrib) #复制tspan，并追加ele_a
                text.remove(tspan) #去掉后，在后面追加(修改)
            tspan_new.append(ele_a)
            text.insert(0, tspan_new) # 追加

            stat = stat + 1
            logger.debug(f"Add a link for text: {cnxt} -- {script}")

        logger.debug(f"Finish link process: pattern={linkPatt} stat={stat}/{total}")

        return self

    def merge(self, *svgs:list['SvgFile']) -> None:
        """把其它SVG文件的内容作为一个<g/>元素合并到当前SVG文件中
        * svgs: 需要合并入当前SVG文件的列表
        Note:
        - 支持修改：每次合并前会去除之前合并的内容（根节点下同名g元素)
        - 支持删除: 只要合并内容非空才会合并
        """
        prefix = "{http://www.w3.org/2000/svg}"
        assert self.root.tag=="{http://www.w3.org/2000/svg}svg", f"First merge SVG file MUST start with <svg> tag. But now the tag is {self.root.tag}!"

        for svg in svgs:

            old = self.root.find(f"{prefix}g[@id='{svg.name}']")  # 查找之前合并的相同id的元素
            if old: self.root.remove(old) # 去除老的元素
            if svg.root.tag!="None":  # 有内容，则合并新元素
                self.root.append(svg.root)
            logger.info(f"Merge a SVG with name={svg.name}")

        logger.info("All SVG file merged.")

        return self
    
    def output(self, hasNS:bool=False, firstG:bool=False, firstGShow:str="none"):
        """输出最终结果内容到文件
        * fileName: 输出文件名
        * hasNS: 是否包含namespace
        """

        if hasNS and self.ns: 
            self.root.attrib['xmlns'] = self.ns[1:-1]

        if firstG:
            page_g = self.root.find('g')
            if page_g!=None:
                page_g.set('style', f'display:{firstGShow}')
                self.root = page_g

        return self

    def write(self, fileName:str):
        """输出最终结果内容到文件
        * fileName: 输出文件名
        * hasNS: 是否包含namespace
        """

        with open(fileName, "w", encoding="utf-8") as f:
            f.write(ET.tostring(self.root, encoding="unicode"))
        logger.info(f"Dump the result content into file: {fileName}.")

        return self
    
    def inject(self, src:str, id:str=''):
        """把当前SVG注入到src文件中"""

        with open(src, "r+", encoding="utf-8") as f:
            context = f.read()
            svgText = ET.tostring(self.root, encoding="unicode")
            patt = f'<div(.*?) id=["\']{id}["\'](.*?)>((?:.|\n)*?)<div>'
            def repl(m): return f'<div{m.group(1)} id="{id}"{m.group(2)}>{svgText}</div>'
            context = re.sub(patt, repl, context)
            f.seek(0)
            f.write(context)

        return self


    def injectToXml(self, src:str, id:str,  out:str="", layer:list[str]=[]):
        """把当前SVG注入到out文件中"""
        assert src!= "", f"Output file name cannot be empty"
        assert Path(src).exists(), f"Cannot find valid file: {src}"
        assert id!="", f"id 标识不能为空"

        tree = ET.parse(src)
        root = tree.getroot()

        radio_xml = ""
        if len(layer):
            radio_xml = f"""<fieldset><legend>Choose Flow:</legend>"""
            for lay in ["None"] + layer:
                rad = f"""<input type="radio" id="{lay}" name="flow" value="{lay}" onchange="flowShow(this, {layer})"/><label for="flow">{lay}</label>"""
                radio_xml = radio_xml + rad

            radio_xml = radio_xml + """</fieldset>
            <script>
              function flowShow(rdo, layers){
                for(lay of layers)
                  document.getElementById(rdo).style.display = "none"
                name = rdo.getAttribute("value")
                if( name != "None" )
                  document.getElementById(name).style.display = "block"
              }
            </script>
        """

        ele = root.find(f'.//div[@id="{id}"]')
        if ele!=None:  # 情况
            ele_attrib = ele.attrib
            ele.clear()
            ele.attrib = ele_attrib
            #if radio_xml: ele.append(ET.fromstring(radio_xml))
            ele.append(self.root)
            logger.info(f"Inject SVG <{self.name}> into {out}.")
        else:
            logger.warning(f"找不到有效的插入点")

        if out=="": out = src
        tree.write(out)

        return self



    def show(self):
        """显示当前SVG内容"""
        return ET.tostring(self.root, encoding="unicode")


#### =======================================================================================================
#### Config文件处理合集
#### =======================================================================================================
class SvgConfigProcess:
    """处理Config文件"""

    def __init__(self, cfgFile:str):
        """
        * cfgFile: Config file path and name
        """
        assert Path(cfgFile).exists(), f"Cannot find config file <{cfgFile}>!"

        with open(cfgFile, "r", encoding="utf-8") as f:
            repl = re.sub( r'//.*[\n]', '\n', f.read() )
            try:
                self.cfg = json.loads( repl )
            except ValueError:
                logger.debug(repl)

        self.svg = SvgFile()

    def process(self):

        assert 'src' in self.cfg, f"Config file should include 'src' field!"

        self.svg.read( self.cfg['src'] )
        self.svg.addLinks2(self.cfg.get("links", {}))

        svgs = []
        for layer in self.cfg.get('layer', []): #### 处理子文件
            assert isinstance(layer, dict), f"Layer config MUST be dict format"
            svgFile = layer.get('src', '')
            # if not svgFile: continue

            svg = SvgFile().read(layer.get('src', ''))
            svg.addLinks2( layer.get('links', {}) )
            svg.toLayer( 
                layer.get('name', re.sub(r'[.].*', '',svgFile)), 
                layer.get('x', 0), 
                layer.get('y', 0),
                display = layer.get('display', "none"),
                delBkRect = layer.get("delBkRect", True)
            )
            svgs.append(svg)

        self.svg.merge( *svgs )

        if 'out' in self.cfg:
            self.svg.write( self.cfg['out'] )

        if 'inject' in self.cfg:
            inject = self.cfg['inject']
            self.svg.injectToXml(inject.get('in', ''), inject.get('out', ''))


#### =======================================================================================================
#### 主程序函数定义
#### =======================================================================================================
def read_json_dict(param:str) -> dict:
    try:
        repl = re.sub( r'//.*[\n]', '\n', param )
        parse1 = json.loads(repl)  # parse1为返回值
    except ValueError:    # 参数为文件名
        paths = param.split('.')   # 使用'.'分割字符串
        fileName, path = f'{paths[0]}.{paths[1]}', paths[2:]
        if not Path(fileName).exists():     # 带扩展名的文件名
            if Path(paths[0]).exists():     # 无扩展名的文件名
                fileName, path = paths[0], paths[1:]    # 修改默认参数
            else:
                raise ValueError(f"Cannot find JSON file from parameter: {param}")
        with open(fileName, "r", encoding="utf-8") as f:
            parse1 = json.loads(f.read())
        parse1 = re.sub( r'//.*[\n]', '\n', parse1 )
        for p in path:  # 遍历路径
            assert p in parse1, f"Cannot find field: {p} in parse1"
            parse1 = parse1[p]  # 深入到路径内
    assert isinstance(parse1, dict), f"Parameter  MUST be dict type"
    return parse1

def parse_src_dst(args):
    src = args.input
    assert src!= "", f"Output file name cannot be empty"
    assert Path(src).exists(), f"Cannot find valid file: {src}"
    dst = args.output if args.output else src
    return (src,dst)


def generate_config_template_file(args) -> None:
    """生成配置文件模板"""
    fileName = args.output if args.output else 'xmler.json'
    with open(fileName, "w", encoding="utf-8") as f:
        f.write(content)
        logger.info(f"\n{content}")
    logger.info( f"Create a template config file to {fileName}." )

def run_config(args) -> None:
    cfgProc = SvgConfigProcess(args.input)
    cfgProc.process()
    logger.info("Finish all operators. Thanks!")

    

#### =======================================================================================================
#### 主程序定义
#### =======================================================================================================
if "__main__" == __name__:

    desp = """
    xmler is a XML file process tools. It supports add, update and delete Id, Hyperlink, Tips, onclick and onContextMenu actions for a <text>.
    The output format is: <text><tspan><a onclick="javascript:{left}" onContextMenu="javascript:{right}" href="{href}" id="{id}">Key<title>{title}</title></a></tspan></text>

    支持如下特性:
    * 支持如下原始标签
      * text文字：<text>Key</text>
      * span文字: <text><tspan>Key</tspan></text>
      * a文字: <text><tspan><a>Key</a></tspan></text>
    * comment插入: 修改一个text标签为:
      <text><span><a id='{cnxt}' title='{title}' href='{href}' onclick='javascript:{left}' oncontext='javascript:{right}'>{cnxt}</a></span></text>
    """

    epilog = textwrap.dedent("""""")

    parse = argparse.ArgumentParser(description=desp, epilog=epilog, formatter_class=SmartDescriptionFormatter)
    if len(sys.argv)==1:
        parse.print_help()
        parse.exit()
    subparsers = parse.add_subparsers(help="sub-command help")

    #### 全局参数
    parse.add_argument('-l', '--log', default=None, help=textwrap.dedent('''Log file name(base on current script execute path). Ignore means no log output'''))

    #### 产生配置模板文件子命令
    parser_gen_cfg = subparsers.add_parser('cfg', help=f"""生成配置模板文件(JSON)。用户可以修改该模板文件来实现自己的脚本。""")
    parser_gen_cfg.add_argument('-o', '--output', default='config.json', help='输出的配置文件名(相对于当前路径)。缺省为config.json')
    parser_gen_cfg.set_defaults(func=generate_config_template_file)

    #### 按照配置文件输出
    parser_run = subparsers.add_parser('run', help="按照配置文件运行")
    parser_run.add_argument('-i', '--input', help="配置文件名(相对于当前路径)")
    parser_run.set_defaults(func=run_config)

    args = parse.parse_args()   # 解析脚本输入的参数

    #### -------------------------- logging文件设置
    # formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(lineno)s %(message)s",datefmt="%Y-%m-%d %H:%M:%S")
    formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(lineno)s %(message)s",datefmt="%Y-%m-%d %H:%M:%S")

    #### 日志输出设置
    if args.log:
        fh = logging.FileHandler(args.log, encoding="utf-8")
        fh.setLevel(logging.DEBUG)
        fh.setFormatter(formatter)
        logger.addHandler(fh)
        logger.info(f"Set log file to {args.log}.")

    #### 终端输出设置
    ch = logging.StreamHandler()
    ch.setLevel(logging.INFO)  # 设置显示级别
    ch.setFormatter(formatter)
    logger.addHandler(ch)
    #### -------------------------------------------

    args.func(args) # 执行解析的子命令函数


